package com.wxt.asmplugins

import com.android.build.api.transform.*
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode

import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream


/**
 * Created by wanglei on 2017/3/17.
 */
class GrovvyTransform extends Transform {
    private final Project project
    private static ArrayList<String> mInjectMethods = new ArrayList<>();
    //免注入名单。列表路径下的类不会被注入
    private static final String WHITELISTFILE = "wmda-rules.properties";
    private static Set<String> mWhiteList = null;
    static {
        mInjectMethods.add("onClick(Landroid/view/View;)V")
        mInjectMethods.add("onClick(Landroid/content/DialogInterface;I)V")
        mInjectMethods.add("onItemClick(Landroid/widget/AdapterView;Landroid/view/View;IJ)V")
    }

    GrovvyTransform(Project project) {
        this.project = project
    }

    String getName() {
        return "GrovvyTransform"
    }

    Set<QualifiedContent.ContentType> getInputTypes() {
        return Collections.singleton(QualifiedContent.DefaultContentType.CLASSES)
    }

    Set<QualifiedContent.Scope> getScopes() {
        Set s = new HashSet();
        s.add(QualifiedContent.Scope.PROJECT)
        s.add(QualifiedContent.Scope.PROJECT_LOCAL_DEPS)
        s.add(QualifiedContent.Scope.SUB_PROJECTS)
        s.add(QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS)
        s.add(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
        return Collections.unmodifiableSet(s);
    }

    boolean isIncremental() {
        return false
    }

    public void getWhiteList() {
        try {
            new File('.').eachFile {
                if (it.getName().equalsIgnoreCase(WHITELISTFILE)) {
                    Properties properties = new Properties()
                    properties.load(it.newDataInputStream())
                    mWhiteList = properties.stringPropertyNames()
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        quietLog(project, "---- my inject start----")
        getWhiteList();
        inputs.each { TransformInput input ->
            input.directoryInputs.each {
                DirectoryInput directoryInput ->
                    def dest = outputProvider.getContentLocation(directoryInput.name,
                            directoryInput.contentTypes, directoryInput.scopes,
                            Format.DIRECTORY)
                    dest.deleteDir()
                    dest.mkdirs()
                    debugLog(project, "Input directory = ${directoryInput.name}")
                    debugLog(project, "Input directory path = ${directoryInput.getFile().getPath()}")
                    int pathBitLen = directoryInput.file.toString().length()
                    directoryInput.file.traverse {
                        def path = "${it.toString().substring(pathBitLen)}"
                        if (it.isDirectory()) {
                            new File(dest, path).mkdirs()
                        } else {
                            byte[] writeBytes = it.bytes
                            if (checkInjectCondition(path)) {
                                try {
                                    System.out.println("====开始inject===")
                                    writeBytes = doInject(writeBytes, project, path)
                                    System.out.println("====inject 结束===")
                                } catch (Exception e) {
                                    System.out.println("====inject 失败===")
                                    errorLog(project, e.getMessage())
                                    writeBytes = it.bytes
                                }
                            }
                            new File(dest, path).bytes = writeBytes
                        }
                    }
            }

            input.jarInputs.each { JarInput jarInput ->
                def jarName = jarInput.name
                def dest = outputProvider.getContentLocation(jarName,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtil.copyFile(jarInput.file, dest)
                debugLog(project, "Input jar = ${jarInput.name}")
                debugLog(project, "Input jar path = ${jarInput.getFile().getPath()}")

                ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest))
                System.out.println("OutPut jar ========= ${dest.name}")
                ZipFile zipFile = new ZipFile(jarInput.file)
                traversalZip(zipFile, { ZipEntry zipEntry, byte[] bytes ->
                    String path = zipEntry.getName()
                    byte[] writeBytes = bytes
                    if (checkInjectCondition(zipEntry.name)) {
                        try {
                            writeBytes = doInject(writeBytes, project, path)
                        } catch (Exception e) {
                            errorLog(project, e.getMessage())
                            writeBytes = bytes
                        }
                    }
                    zipEntry = new ZipEntry(zipEntry.getName())
                    zos.putNextEntry(zipEntry)
                    zos.write(writeBytes)
                    zos.closeEntry()
                })
                zos.close()
            }
        }
        quietLog(project, "---- my inject end----")
    }

    static boolean checkInjectCondition(String className) {
        String newClassName = className.replaceAll("\\\\", "/");
        if (mWhiteList != null && mWhiteList.size() > 0) {
            Iterator iterator = mWhiteList.iterator();
            while (iterator.hasNext()) {
                String blackPackageName = iterator.next()
                if (newClassName.contains(blackPackageName)) {
                    return false;
                }
            }
        }
        return (className.endsWith(".class")
                && !className.contains('R$')
                && !className.contains('R.class')
                && !className.contains('BuildConfig.class')
                && !className.contains('Test.class'))
    }

    static byte[] doInject(byte[] writeBytes, project, path) {
        System.out.println("=====inject path===" + path);
        ClassReader classReader
        classReader = new ClassReader(writeBytes)
        writeBytes = checkFragment(classReader, writeBytes, project, path)
        classReader = new ClassReader(writeBytes)
        writeBytes = checkSupportFragment(classReader, writeBytes, project, path)
        classReader = new ClassReader(writeBytes)
        System.out.println("=====create checkClick===");
        writeBytes = checkClick(classReader, writeBytes, project, path)
        return writeBytes
    }

    static byte[] checkFragment(ClassReader classReader, byte[] inputBytes, Project project, String path) {
        if ('android/app/Fragment'.equals(classReader.getSuperName())) {
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
            ClassVisitor classAdapter = new FragmentInjector(classWriter, project)
            classReader.accept(classAdapter, 12)
            quietLog(project, "Inject Fragment code : ${path}")
            return classWriter.toByteArray()
        } else {
            return inputBytes
        }
    }

    static byte[] checkSupportFragment(ClassReader classReader, byte[] inputBytes, Project project, String path) {
        if ('android/support/v4/app/Fragment'.equals(classReader.getSuperName())) {
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
            ClassVisitor classAdapter = new SupportFragmentInjector(classWriter, project)
            classReader.accept(classAdapter, 12)
            quietLog(project, "Inject V4Fragment code : ${path}")
            return classWriter.toByteArray()
        } else {
            return inputBytes
        }
    }

    static byte[] checkClick(ClassReader classReader, byte[] inputBytes, Project project, String path) {
        ClassNode node = new ClassNode();
        classReader.accept(node, ClassReader.EXPAND_FRAMES);
        if ((node.access & Opcodes.ACC_INTERFACE) == Opcodes.ACC_INTERFACE) {
            return inputBytes
        }
        boolean needInject = needInjectOnClick(node)
        if (needInject) {
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
            Utils.log("==create OnClickInjector=======" + project + "=======" + path)
            ClassVisitor classAdapter = new OnClickInjector(classWriter, project, path)
            classReader.accept(classAdapter, 12)
            return classWriter.toByteArray()
        } else {
            return inputBytes
        }
    }

    static boolean needInjectOnClick(ClassNode cn) throws IOException {
        List<MethodNode> methods = cn.methods;
        for (int i = 0; i < methods.size(); ++i) {
            MethodNode method = methods.get(i);
            System.out.println(" find OnClick Method " + method.name)
            if (mInjectMethods.contains(method.name + method.desc) && ((method.access & Opcodes.ACC_ABSTRACT) != Opcodes.ACC_ABSTRACT)) {
                return true;
            }
        }
        return false
    }

    static void traversalZip(ZipFile zipFile, Closure callback) {
        try {
            Enumeration<? extends ZipEntry> enumeration = zipFile.entries();
            while (enumeration.hasMoreElements()) {
                ZipEntry entry = enumeration.nextElement();
                callback.call(entry, zipFile.getInputStream(entry).bytes)
            }
        } catch (IOException e) {
            e.printStackTrace();
            closeQuietly(zipFile);
        }
    }

    static void closeQuietly(Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static void errorLog(Project project, String log) {
        project.logger.error "[error log ] " + log
    }

    static void quietLog(Project project, String log) {
        project.logger.quiet "[quietLog log] " + log
    }

    static void debugLog(Project project, String log) {
        project.logger.debug "[debugLog log] " + log
    }

}
